package edu.upenn.cis.cis555.webserver;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Vector;
import java.util.regex.Pattern;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;

public class WorkerThread extends Thread {

	private Socket connectionsocket;
	private int httpVersion;
	private int method;
	private int type_is;
	private int contentLength;
	private BufferedReader input;
	private DataOutputStream output;
	private StringBuffer requestURI;
	private String path;
	private String servletMethod;
	private String line1;
	private Vector<String> otherlines;
	private HashMap<String,ArrayList<String>> reqHeaders;
	private StringBuffer reqBody;
	private Vector<Cookie> cookies;
	private HttpServlet servlet;
	private String url_matched;
	private boolean patternMatched;

	public synchronized void run() {
		while(true) {
			synchronized(HttpServer.workQ.getQ()) {
				while (HttpServer.workQ.getQ().isEmpty()) {
					try{
						HttpServer.workQ.getQ().wait();
					}
					catch (InterruptedException e) {
						Thread.currentThread().interrupt();
						System.out.println(Thread.currentThread().getName() +" Terminated");
				        return;
					}
				}
				connectionsocket = HttpServer.workQ.getQ().removeFirst();
			}
			try {
				process_request();
			} catch (IOException e) {
				HttpServer.logError("ProcessRequestError " + e.getMessage());
			}
		}
	}

	// <---------------- Determine Request Type ---------------->
	public void process_request() throws IOException{
		input = new BufferedReader(new InputStreamReader(connectionsocket.getInputStream()));
		output = new DataOutputStream(connectionsocket.getOutputStream());
		httpVersion = -1;
		method = 0; //1 get, 2 head, 3 post, 0 not implemented
		path = new String();
		servletMethod = "N/A";
		otherlines = new Vector<String>();
		reqHeaders = new HashMap<String,ArrayList<String>>();
		reqBody = new StringBuffer();
		try {
			line1 = input.readLine();
			if (line1.contains("HTTP/1.1"))
				httpVersion = 1;
			if (line1.contains("HTTP/1.0"))
				httpVersion = 0;
			if (line1.startsWith("GET")) {
				method = 1;
				servletMethod = "GET";
			}
			if (line1.startsWith("HEAD")) {
				method = 2;
			}
			if (line1.startsWith("POST")) {
				method = 3;
				servletMethod = "POST";
			}

			Pattern p = Pattern.compile(" ");
			String[] result = p.split(line1);
			requestURI = new StringBuffer(result[1]);
			StringBuffer temp = new StringBuffer(requestURI.toString());
			path = temp.deleteCharAt(0).toString();
			
			String urltomatch;
			StringBuffer rURI = new StringBuffer(requestURI);
			if (rURI.toString().contains("?")) {
				Pattern p2 = Pattern.compile("?", Pattern.LITERAL);;
				String[] result2 = p2.split(rURI.toString());
				urltomatch = result2[0];
			}
			else {
				urltomatch = requestURI.toString();
			}

			for (String urlPattern : HttpServer.m_urls.keySet()) {
				if (urltomatch.equals(urlPattern)) {
					patternMatched = false;
					url_matched = urlPattern;
					servlet = HttpServer.servlets.get(HttpServer.m_urls.get(urlPattern));
					servlet_request();
					return;
				}
				if (urlPattern.contains("*")) {
					Pattern p1 = Pattern.compile("*", Pattern.LITERAL);
					String[] result1 = p1.split(urlPattern);
					if (requestURI.toString().startsWith(result1[0])) {
						patternMatched = true;
						url_matched = result1[0];
						servlet = HttpServer.servlets.get(HttpServer.m_urls.get(urlPattern));
						servlet_request();
						return;
					}
				}
			}
		}
		catch (Exception e) {
			HttpServer.logError("Process: Status 500 " + e.getMessage());
			error500();
			return;  
		}
		http_request();
	}

	// <---------------- Process Servlet Request ---------------->
	public void servlet_request(){

		//		if it is a servlet then:
		//			read the headers of the request to check for cookies or content-length (meaning the request has a body)
		//			check for the session stuff
		//			make a request and call all the various methods like “setMethod” and “setParameter”
		//			make a response and pass the output stream and maybe a cookie
		//			call servlet.service(request, response)
		//			flush and close the output stream

		try {
			String theLine;
			
			//Read in headers
			while (true) {
				Pattern p1 = Pattern.compile(":");;
				theLine = input.readLine();
				String[] result1 = p1.split(theLine);
				if (result1.length >= 2) {
					ArrayList<String> temp = new ArrayList<String>();
					temp.add(result1[1].trim());
					String key = result1[0].trim().toLowerCase();
					if (!reqHeaders.containsKey(key)) {
						reqHeaders.put(key, temp);
					}
					else {
						reqHeaders.get(key).add(result1[1].trim());
					}
				}
				else 
					break;
			}
			
			//Return 400 if HTTP/1.0 and no Host: provided
			if (httpVersion == 1 && !reqHeaders.keySet().contains("host")) {
				output.writeBytes(construct_http_header(httpVersion, 400, type_is, contentLength));
				output.writeBytes("<HTML><HEAD><TITLE>400 Bad Request</TITLE></HEAD><BODY>");
				output.writeBytes("<h1>Bad Request</h1>");
				output.writeBytes("<p>The request is not HTTP/1.1 compliant and cannot be processed</p>");
				output.writeBytes("</BODY></HTML>");
				output.close();
				return;
			}
			
			cookies = new Vector<Cookie>();
			
			HttpSession theSession = null;
			
			// Is there a session associated with this client request?
			for (String key : reqHeaders.keySet()) {
				if (key.equals("cookie")) {
					Pattern p1 = Pattern.compile("; ");;
					String cookieline = reqHeaders.get(key).get(0);
					String[] result = p1.split(cookieline);
					for (String ele : result) {
						Pattern p2 = Pattern.compile("=");;
						String[] result2 = p2.split(ele);
						if (!result2[0].equals("JSESSIONID")) {
							Cookie acookie = new Cookie(result2[0], result2[1]);
							cookies.add(acookie);
							continue;
						}
						else {
							for (HttpSession asession : HttpServer.sessions) {
								if (asession.getId().equals(result2[1])) {
									theSession = asession;
								}
							}
						}
					}
				}
			}
			
			// If session too old --> invalidate()
			if (theSession != null) {
				if (theSession.getMaxInactiveInterval() >= 0 && theSession.isValid()) {
					if (theSession.isNew()) {
						if ((System.currentTimeMillis() - theSession.getCreationTime()) > theSession.getMaxInactiveInterval()*1000) {
							theSession.invalidate();
						}
					}
					else {
						if ((System.currentTimeMillis() - theSession.getLastAccessedTime()) > theSession.getMaxInactiveInterval()*1000) {
							theSession.invalidate();
						}
					}
				}
				theSession.setLastAccessedTime(System.currentTimeMillis());
			}
			
			// Create request, pass in some information
			HttpServletRequest request = new HttpServletRequest(
					theSession, requestURI, connectionsocket, line1, 
					reqHeaders, servletMethod, cookies, url_matched, patternMatched
					);
			
			// If POST with content-length and content-type, read in body to set parameters
			if (servletMethod.equals("POST") && request.getIntHeader("content-length") > 0 && reqHeaders.containsKey("content-type")) {
				if (request.getHeader("content-type").equals("application/x-www-form-urlencoded")) {
					while (input.ready()) {
						reqBody.append((char)input.read());
					}
					String params = new String(reqBody.toString().replaceAll("\\s", ""));
					Pattern p1 = Pattern.compile("&", Pattern.LITERAL);;
					String[] result = p1.split(params);
					
					for (int i = 0; i < result.length; i++) {
						Pattern p2 = Pattern.compile("=", Pattern.LITERAL);
						String[] result2 = p2.split(result[i]);
						String key = URLDecoder.decode(result2[0], request.getCharacterEncoding());
						String value = URLDecoder.decode(result2[1], request.getCharacterEncoding());
						request.setParameter(key, value);
					}
				}
			}
			
			// If query string, set parameters
			if (request.getQueryString() != null) {
				Pattern p1 = Pattern.compile("&", Pattern.LITERAL);;
				String[] result = p1.split(request.getQueryString());
				for (int i = 0; i < result.length; i++) {
					Pattern p2 = Pattern.compile("=", Pattern.LITERAL);
					String[] result2 = p2.split(result[i]);
					String key = URLDecoder.decode(result2[0], request.getCharacterEncoding());
					String value = URLDecoder.decode(result2[1], request.getCharacterEncoding());
					request.setParameter(key, value);
				}
			}
			
			// Create response, pass in some information
			HttpServletResponse response = new HttpServletResponse(output, httpVersion, cookies);
			
			if (servlet == null) {
				HttpServer.logError("Cannot Find Servlet Mapping");
				return;
			}
			if (servletMethod.compareTo("GET") == 0 || servletMethod.compareTo("POST") == 0) {
				servlet.service(request, response);
				response.flushBuffer();
				output.close();
			} else {
				HttpServer.logError("Servlet Must Accept GET/POST");
				return;
			}
		}
		catch (Exception e) {
			HttpServer.logError("Servlet: Status 500" + e.getMessage());
			error500();
			return; 
		}
	}

	
	// <---------------- Process normal http_request ---------------->
	public void http_request(){
		String line2;
		type_is = 0;
		contentLength = -1;
		File reqfile = null;
		FileInputStream requestedfile = null;

		try {
			if (httpVersion == -1) { // 505, Http version not supported
				output.writeBytes(construct_http_header(1, 505, type_is, contentLength));
				output.writeBytes("<HTML><HEAD><TITLE>505 Http Version Not Supported</TITLE></HEAD><BODY>");
				output.writeBytes("<h1>Http Version Not Supported</h1>");
				output.writeBytes("<p>The request is not HTTP/1.1 or 1.0 compliant and cannot be processed</p>");
				output.writeBytes("</BODY></HTML>");
				output.close();
				return;
			}
			StringBuffer temp3 = new StringBuffer(path);
			temp3.insert(0, new String(HttpServer.root_path + "/"));
			reqfile = new File(temp3.toString());
			requestedfile = new FileInputStream(reqfile);

			if (path.endsWith(".jpg") || path.endsWith(".jpeg") || path.endsWith(".JPG")) {
				type_is = 1;
			}
			if (path.endsWith(".gif") || path.endsWith(".GIF")) {
				type_is = 2;
			}
			if (path.endsWith(".png") || path.endsWith(".PNG")) {
				type_is = 3;
			}
			if (path.endsWith(".txt") || path.endsWith(".TXT")) {
				type_is = 4;
			}

			contentLength = requestedfile.available();

			line2 = input.readLine();
			if (httpVersion == 1 && !line2.contains("Host:")) { // 400 Bad Request, no Host specified
				output.writeBytes(construct_http_header(httpVersion, 400, type_is, contentLength));
				output.writeBytes("<HTML><HEAD><TITLE>400 Bad Request</TITLE></HEAD><BODY>");
				output.writeBytes("<h1>Bad Request</h1>");
				output.writeBytes("<p>The request is not HTTP/1.1 compliant and cannot be processed</p>");
				output.writeBytes("</BODY></HTML>");
				output.close();
				return;
			}

			if (method == 0) { // 501, not implemented
				output.writeBytes(construct_http_header(httpVersion, 501, type_is, contentLength));
				output.writeBytes("<HTML><HEAD><TITLE>501 Not Implemented</TITLE></HEAD><BODY>");
				output.writeBytes("<h1>Not Implemented</h1>");
				output.writeBytes("<p>This server does not support the functionality required to fulfill this request.</p>");
				output.writeBytes("</BODY></HTML>");
				output.close();
				return;
			}

			while (input.ready()) {
				otherlines.add(input.readLine());
			}
			for (int i = 0; i < otherlines.size(); i++) {
				if (otherlines.get(i).contains("If-Modified-Since: ")) {
					Pattern p1 = Pattern.compile("If-Modified-Since: ");
					String[] result1 = p1.split(otherlines.get(i));
					StringBuffer tmp5 = new StringBuffer(result1[1]);
					SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
					Date d1 = format.parse(tmp5.toString());
					if (d1.getTime() > reqfile.lastModified()) { // 304 Not Modified Since Error
						output.writeBytes(construct_http_header(httpVersion, 304, type_is, contentLength));
						output.writeBytes("<HTML><HEAD><TITLE>304 Not Modified</TITLE></HEAD><BODY>");
						output.writeBytes("<h1>Not Modified</h1>");
						output.writeBytes("<p>The requested file's last modified date is older than that specified in the request</p>");
						output.writeBytes("</BODY></HTML>");
						output.close();
						requestedfile.close();
						return;        	        	
					}            		            		
				}
			}
			//Outside of root directory, Forbidden access, then status code 403
			// telnet localhost 8081; GET /../index.html HTTP/1.1 returns status code 403
			if (!reqfile.getCanonicalPath().endsWith(temp3.toString())) {
				output.writeBytes(construct_http_header(httpVersion, 403, type_is, contentLength));
				output.writeBytes("<HTML><HEAD><TITLE>403 Forbidden</TITLE></HEAD><BODY>");
				output.writeBytes("<h1>Forbidden</h1>");
				output.writeBytes("<p>Access to the requested URL is forbidden.</p>");
				output.writeBytes("</BODY></HTML>");
				output.close();
				requestedfile.close();
				return;
			}
		}
		catch (Exception e) {
			try {
				// if directory, return listing of files in directory
				if (reqfile.isDirectory()) {
					output.writeBytes(construct_http_header(httpVersion, 200, type_is, contentLength));
					output.writeBytes("<HTML><HEAD><TITLE>http://"+connectionsocket.getLocalAddress().getHostName());
					output.writeBytes(":"+connectionsocket.getLocalPort()+"/"+path+"</TITLE></HEAD><BODY>");
					output.writeBytes("<h1>Current Directory Listing</h1>");
					File[] children = reqfile.listFiles();
					for (int i = 0; i < children.length; i++)
						output.writeBytes("<a href=" + children[i].getCanonicalPath().replace(HttpServer.root_path, "") + ">" + children[i].getName() + "</a><br>");
					output.writeBytes("</BODY></HTML>");
					output.close();
					requestedfile.close();
					return;
				}
				// if can't open file, then status code 404
				output.writeBytes(construct_http_header(httpVersion, 404, type_is, contentLength));
				output.writeBytes("<HTML><HEAD><TITLE>404 Not Found</TITLE></HEAD><BODY>");
				output.writeBytes("<h1>Not Found</h1>");
				output.writeBytes("<p>The requested URL was not found on this server.</p>");
				output.writeBytes("</BODY></HTML>");
				output.close();
			}
			catch (Exception e2) {
				HttpServer.logError("File: Status 500 " + e.getMessage());
				error500();
				return; 	
			}
		}
		try {

			output.writeBytes(construct_http_header(httpVersion, 200, type_is, contentLength));

			if (method == 1) { //if HEAD request, do not do BODY
				while (true) {
					int incBytes = requestedfile.read();
					if (incBytes == -1) {
						break;
					}
					output.write(incBytes);
				}
			}
			output.close();
			requestedfile.close();
		}
		catch (Exception e) {}
	}

	private String construct_http_header(int httpVersion, int status_code, int file_type, int numBytes) {
		String s = "HTTP/1." + httpVersion + " ";
		switch (status_code) {
		case 100:
			s += "100 Continue";
			s += "\r\n";
			return s;
		case 200:
			s += "200 OK";
			break;
		case 304:
			s += "304 Not Modified";
			break;
		case 400:
			s += "400 Bad Request";
			break;
		case 403:
			s += "403 Forbidden";
			break;
		case 404:
			s += "404 Not Found";
			break;
		case 500:
			s += "500 Internal Server Error";
			break;
		case 501:
			s += "501 Not Implemented";
			break;
		case 505:
			s += "505 Http Version Not Supported";
			break;
		}
		
		if (status_code != 200)
			HttpServer.logError(s.substring(8));

		s += "\r\n";

		switch (file_type) {
		case 0:
			break;
		case 1:
			s += "Content-Type: image/jpeg\r\n";
			break;
		case 2:
			s += "Content-Type: image/gif\r\n";
			break;
		case 3:
			s += "Content-Type: image/png\r\n";
			break;
		case 4:
			s += "Content-Type: text/plain\r\n";
			break;
		default:
			s += "Content-Type: text/html\r\n";
			break;
		}
		Calendar cal = Calendar.getInstance();
		SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM HH:mm:ss zzz yyyy");
		s += ("Date: " + format.format(cal.getTime()));
		s += "\r\n";
		if (numBytes != -1){
			s += "content-length:" + numBytes;
			s += "\r\n";
		}
		s += "Server: Http Server\r\n";
		if (httpVersion == 1)
			s += "Connection: close\r\n"; // Have not handled Persistent Connections yet
		s += "\r\n";	
		return s;
	}
	
	private void error500() {
		try {
			output.writeBytes(construct_http_header(httpVersion, 500, type_is, contentLength));
			output.writeBytes("<HTML><HEAD><TITLE>500 Internal Server Error</TITLE></HEAD><BODY>");
			output.writeBytes("<h1>Internal Server Error</h1>");
			output.writeBytes("<p>Something went wrong within the server</p>");
			output.writeBytes("</BODY></HTML>");
			output.close();
		}
		catch (IOException e) {
			HttpServer.logError("Output: Status 500 " + e.getMessage());
		}
	}
}